Marketing Mix Modelling¶

Case study: FourTex¶

Introduction¶

fourtex.png

FourTex is an apparel brand. Its products are targeted at the mass market with a focus on casual apparel for men and women. The brand’s advertising is prominent and extensive, and is present on multiple channels such as television, radio, paid search, and social media.

Recently, the digital marketing tools have extended FourTex’s reach among the potential customers. The marketing director, Hannah Schmidt, was so proud of her team’s achievements. She was getting ready for a meeting with the management board to explain how successful their previous marketing campaigns were, and hoping to get some additional budget in order to further improve the brand’s online store visits and sales leads performance metrics.

The meeting did not go as planned. In a difficult conversation, the chief executive officer (CEO) of the company said: “Mrs. Schmidt, you always ask for more money, but can rarely explain how much incremental value this money will generate”. Coming out of the meeting, Mrs. Schmidt felt that she was under enormous pressure to demonstrate the value of her marketing decisions.

Next day, she pulled some data from the company’s database, taking her notes. The table below provides a brief description of her data set:

Marketing Mix Variable Description Channel
1 Cost of Google AdWords campaigns Online
2 Cost of sponsored ads delivered on Facebook Online
3 Cost of TV advertising Offline
4 Cost of radio advertising Offline
5 Total number of visits to the website Online

Being very keen to demonstrate marketing’s value, Mrs. Schmidt asked her analytics team to assess the impact of their Google AdWords, Facebook, TV, and radio ads on website traffic performance. She did not want to include the sales performance metric in the analysis because her campaigns last year aimed to increase conversion to the website rather than the sales outcomes. Therefore, she thought that the relevant key performance indicator was website traffic.

Finally, Mrs. Schmidt prepared a checklist for the analytics team. She seeks answers to the following questions:

  • Which marketing mix instrument really drives the performance outcome, i.e. website traffic?

  • What is the return on marketing investment?

  • Should I keep pushing on with Google adwords and Facebook ads? Should I stop advertising on TV and radio channels?

How can Mrs. Schmidt demonstrate the impact of her marketing mix decisions to the management board?

To address Mrs. Schmidt’s questions, we will build a marketing mix model that gauges the effectiveness of her marketing mix decisions and estimates the contribution of each advertising vehicle to website traffic performance.

Before we roll out the analyses, we should make sure that all our files are organised and our R environment is set up.

Preparation and set-up¶

Let's follow the below steps:

  • Create a folder on your computer and name the folder (e.g., fourtex)

  • Download the data to the folder you just created.

  • Open your JupyterLab and launch a new ipynb file from the 'File' tab. Name your jupyter notebook (ipynb) file (e.g. mmm_fourtex) and save it to your folder.

To set the working directory, please run the following chunk. Ensure that you change the path to yours.

In [2]:
# DON'T FORGET TO EDIT THE PATH

import os
os.chdir('C:/Users/gyildiri/Downloads/chapter 3')

We are now ready to load the dataset to Python and perform some exploratory data analysis to get a feel for the data.

Exploratory data analysis¶

To load the data into our Python environment and execute the codes for the rest of the tutorial, first we need to import the necessary libaries.

In [18]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import statsmodels.api as sm
from sklearn.linear_model import LinearRegression

The following code loads the data and summarizes its main characteristics.

In [19]:
# Load the data
data = pd.read_csv(r'C:\Users\gyildiri\Downloads\chapter 3\data_fourtex.csv')#('/content/data_fourtex.csv')

data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 57 entries, 0 to 56
Data columns (total 6 columns):
 #   Column          Non-Null Count  Dtype  
---  ------          --------------  -----  
 0   week_beg        57 non-null     object 
 1   Google_Adwords  57 non-null     float64
 2   Facebook        57 non-null     int64  
 3   TV              57 non-null     float64
 4   Radio           57 non-null     float64
 5   traffic         57 non-null     int64  
dtypes: float64(3), int64(2), object(1)
memory usage: 2.8+ KB

The data set has 6 variables and 57 time series observations for each variable.

Now, if you check the variables with right click and variable inspector, you will see that the data set is loaded. You can click on the new tab and observe the data.

Next, we would like to extract each of the variables to the environment tab:

The python lists these variables as float, int and object. Python knows the variables we want to work with. However, it does not know that they are time series variables. So, we should get them recognised as time series data by using the pd.to_datetime function.

In [20]:
# Convert week_beg to datetime
data['week_beg'] = pd.to_datetime(data['week_beg'], format='%d/%m/%Y')

# Creating time series for each column with a weekly frequency
data.set_index('week_beg', inplace=True)
data.index = pd.DatetimeIndex(data.index.values, freq=data.index.inferred_freq)

Note that the dataset runs on a weekly basis. We set the frequency of the data to $52$ weeks. However, not all the years have $52$ weeks. Normally, the year has $365.25/7=52.18$ weeks, on average. This allows for a leap year every fourth year. Therefore, some years may have 53 weeks. This is not an issue for our data as we have $57$ observations spanning over two years. None of them covers $53$ weeks.

Plot the data

To get a feel for the data patterns, we need to perform a visual inspection through time-series plots. As an example, we may want to sum up online spending variables to find the total online spending, then do the same for offline spending variables. See below.

In [25]:
# Summing up the online and offline totals
data['online_total'] = data['Google_Adwords'] + data['Facebook']
data['offline_total'] = data['TV'] + data['Radio']

Then, we plot the time series data on total online spending, total offline spending and traffic:

In [28]:
# Plotting the traffic data
plt.figure(figsize=(10, 6))
plt.plot(data['traffic'], color="blue", label="Online traffic")
plt.title("Online traffic")
plt.xlabel("Date")
plt.ylabel("Online traffic")
plt.legend()
plt.show()
No description has been provided for this image
In [29]:
# Plotting the Online spending data
plt.figure(figsize=(10, 6))
plt.plot(data['online_total'], color="green", label="Online spending")
plt.title("Online spending")
plt.xlabel("Date")
plt.ylabel("Online spending")
plt.legend()
plt.show()
No description has been provided for this image
In [30]:
# Plotting the Offline spending data
plt.figure(figsize=(10, 6))
plt.plot(data['offline_total'], color="red", label="Offline spending")
plt.title("Offline spending")
plt.xlabel("Date")
plt.ylabel("Offline spending")
plt.legend()
plt.show()
No description has been provided for this image

Question: Looking at these plots, what do you observe? Do increases and decreases in online traffic coincide with online and offline spending? Do you see any seasonality or trend patterns?


In addition to simple time-series plot analysis, we may want to explore how much the brand spent on online and offline ads over the data period.

In [32]:
# Calculating media spending share
sum_online = data['online_total'].sum()
sum_offline = data['offline_total'].sum()
total_spend = sum_online + sum_offline

online_share = sum_online / total_spend
offline_share = sum_offline / total_spend

# Pie-Chart for Media Spending Share
slices = [online_share, offline_share]
labels = ['Online', 'Offline']
colors = plt.cm.rainbow(np.linspace(0, 1, len(labels)))

plt.figure(figsize=(6, 6))
plt.pie(slices, labels=labels, colors=colors, autopct='%1.1f%%', startangle=140)
plt.title("Ad spending share")
plt.show()
No description has been provided for this image

We see from the pie chart that FourTex spent 77% of its budget on offline ads, while 23% of the budget went on online ads.

Marketing mix modeling¶

Having explored the main features of the data, we are ready to investigate the drivers of web traffic performance. We will develop a multiple regression model that uses the traffic data as dependent variable (i.e., outcome or response variable) and Google AdWords, Facebook, TV, and radio as independent variables, also known as predictors.

Diminishing returns¶

At this point, an important decision we need to make is which functional form to use in the model. Shall we assume a linear or a non-linear relationship? Marketing literature suggests that the relationship between advertising and performance variables mostly follows a diminishing return pattern (Hanssens et al., 2001, 2014), as illustrated in the following plot.

No description has been provided for this image

This plot tells us that initially spending more and more money on advertising is beneficial but after a certain point the additional value gained from an extra spending will be very small. How can we introduce this type of non-linearity in the model? A typical approach is to use a log-log model specification. The log-log regression model suggests that log transformation is performed for both sides of the equation.

Turning to our data application, with the log-log specification our marketing mix model becomes:


$$ \ln(\text{Traffic}_t)= \beta_0 + \beta_1 \ln(\text{Traffic}_{t-1}) + \beta_2 \ln(\text{Adwords}_t) + \beta_3 \ln(\text{Facebook}_t) + \beta_4 \ln(\text{TV}_t) + \beta_5 \ln(\text{Radio}_t) + \epsilon_{t} $$

where ln stands for natural logarithm.


Question: Did you notice that we added the lagged traffic variable as an additional predictor to our model? Do you think including the first lagged traffic variable in the model makes sense? Why?


To estimate the above log-log model, we will perform log transformation on our variables.

In [33]:
# Log transformation of the variables
data['ln_google_adwords'] = np.log(data['Google_Adwords'] + 1)
data['ln_facebook'] = np.log(data['Facebook'] + 1)
data['ln_tv'] = np.log(data['TV'] + 1)
data['ln_radio'] = np.log(data['Radio'] + 1)
data['ln_traffic'] = np.log(data['traffic'] + 1)

Did you notice that we added $+1$ to the original variables? The reason for doing this is that some variables include zero observations. When we take the logarithm of zero, it is not identified. Therefore, we should add a small number to be able to take the logarithm.

We have all the variables to be used in the model, except the first lagged traffic variable in logarithm, $lnTraffic_{t-1})$*. The code chunk below creates this variable.

In [34]:
# Creating lagged traffic variable (1 lag)
data['L1_ln_traffic'] = data['ln_traffic'].shift(1)

Now, let’s run our model in our notebook:

In [37]:
# Dropping NA values created by lag
data_lagged = data.dropna()

# Preparing the data for regression
X = data_lagged[['L1_ln_traffic', 'ln_google_adwords', 'ln_facebook', 'ln_tv', 'ln_radio']]
y = data_lagged['ln_traffic']

# Fit a linear regression model
regression_model = LinearRegression()
regression_model.fit(X, y)
fitted_traffic = regression_model.predict(X)

# Summary of the regression model
model_summary = sm.OLS(y, sm.add_constant(X)).fit().summary()

Let's check the model parameters and the performance with the summary.

In [38]:
model_summary
Out[38]:
OLS Regression Results
Dep. Variable: ln_traffic R-squared: 0.758
Model: OLS Adj. R-squared: 0.734
Method: Least Squares F-statistic: 31.29
Date: Wed, 01 May 2024 Prob (F-statistic): 2.73e-14
Time: 15:01:05 Log-Likelihood: 40.069
No. Observations: 56 AIC: -68.14
Df Residuals: 50 BIC: -55.99
Df Model: 5
Covariance Type: nonrobust
coef std err t P>|t| [0.025 0.975]
const 3.1507 1.498 2.103 0.041 0.142 6.160
L1_ln_traffic 0.5359 0.076 7.008 0.000 0.382 0.690
ln_google_adwords 0.1554 0.090 1.730 0.090 -0.025 0.336
ln_facebook 0.1937 0.056 3.456 0.001 0.081 0.306
ln_tv 0.0046 0.004 1.067 0.291 -0.004 0.013
ln_radio 0.0070 0.004 1.721 0.091 -0.001 0.015
Omnibus: 1.130 Durbin-Watson: 1.382
Prob(Omnibus): 0.568 Jarque-Bera (JB): 0.858
Skew: -0.303 Prob(JB): 0.651
Kurtosis: 2.965 Cond. No. 2.07e+03


Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 2.07e+03. This might indicate that there are
strong multicollinearity or other numerical problems.

Now, let's explore what the model output suggests.

regression_output_python.png

Model Output

Coefficient estimates:

We can write down the estimated coefficients of this marketing mix model as an equation:


$$ \ln(\text{Traffic}_t)= 3.151 + 0.536 \ln(\text{Traffic}_{t-1}) + 0.155 \ln(\text{Adwords}_t) + 0.194 \ln(\text{Facebook}_t) + 0.005 \ln(\text{TV}_t) + 0.007 \ln(\text{Radio}_t) $$

The standard error of each coefficient shows at what precision level we estimated that particular coefficient, i.e., represents the uncertainty surrounding that coefficient. The t-value is obtained by dividing the coefficient by its standard error. The p-value, Pr(>|t|), is computed based on the t-value, and helps us understand whether the coefficient is statistically significant. For example, lagged traffic is a strong indicator of the next period’s traffic as it is highly significant (the p-value is close to zero). The effect of Facebook ads is significant at 1% level while Google AdWords and radio effects are significant at the 10% level. Finally, the effect of TV ads is not statistically significant.

How to interpret the coefficients?

We start with the interpretation of the autoregressive coefficient. The estimated effect size is 0.536. If web traffic performance gains a certain momentum today, we would expect that it will carry over into future periods with an attrition rate of $0.464$ $(1-0.536)$. This implies that some FourTex customers make repeat web visits due to a gained place of the brand or ads in their memories while others stop visiting after some time because their ad or site memory decays rather quickly.

Next, we look at the advertising media effects. Since this is a log-log model output, the estimated coefficients can be interpreted as elasticities:

The expected % change in our response variable with respect to a % change in our predictor variable, holding other predictor variables constant.

To understand this better, let's focus on the effect of Google AdWords. Take two values of Google AdWords at two consecutive periods: A1 and A2. Holding the other variables fixed in the above equation, we obtain the following:

$$ \ln(\text{Traffic}(A2)) - \ln(\text{Traffic}(A1)) = 0.155 \cdot (\ln(\text{Adwords}(A2)) - \ln(\text{Adwords}(A1))) $$

Using the logarithm properties, we can simplify this as follows:

$$ \ln \left( \frac{\text{Traffic}(A2)}{\text{Traffic}(A1)} \right) = 0.155 \cdot \ln \left( \frac{\text{Adwords}(A2)}{\text{Adwords}(A1)} \right) $$

Simplifying the above equation further, we get:

$$ \frac{\text{Traffic}(A2)}{\text{Traffic}(A1)} = \left( \frac{\text{Adwords}(A2)}{\text{Adwords}(A1)} \right)^{0.155} $$

This result suggests that as long as the ratio of the two Google AdWords spending levels, i.e., $\frac{\text{Adwords}(A2)}{\text{Adwords}(A1)}$, stays the same, the expected ratio of the response variable, $\frac{\text{Traffic}(A2)}{\text{Traffic}(A1)}$ stays the same. For example, when we increase Google AdWords by 10%, we expect about a 1.5% increase in web traffic ($1.10^{0.155} = 1.015$).


Question: What is your interpretation of the estimated coefficients of the other advertising variables, Facebook, TV, and radio?


R-squared:

It is also known as the coefficient of determination. It measures the proportion of variation in the response variable explained by the independent variables. In our case, the model we built explains 76% of the variation in logged traffic (see the Multiple R-squared from the regression output).

Adjusted R-squared:

It is possible to increase the R-squared by adding more and more variables to the regression equation. However, the model's explanatory power can be increased just by chance when we add more variables to the model. To see whether a new variable will improve the model, the adjusted R-squared value should be checked. From the regression output, we see that the adjusted R-squared is 0.73. That means that our model did not suffer much from adding more variables (76% for R-squared vs. 73% for adjusted R-squared).

F-test:

We use the F-test to assess whether a linear regression with predictor variables is favored over using only the average value of the response variable. The computed F-statistic is 31.29 with a p-value of 2.73e-14. Indeed, it is a very small number, less than the threshold of 0.05 for a 95% level of statistical significance. Thus, we can conclude that the model we estimated should be favored over a simple mean of sales.

Model Fit

Having estimated the parameters, we can obtain the model fit plot to see visually whether our model captures the patterns in the traffic data. Running the following code gives us the model fit plot below.

In [ ]:
# Plotting the actual vs fitted logged traffic data
plt.figure(figsize=(10, 6))
plt.plot(data_lagged.index, data_lagged['ln_traffic'], label='Logged Traffic Data', color='blue', lw=2)
plt.plot(data_lagged.index, fitted_traffic, label='Fitted', color='red', linestyle='--')
plt.title('Web Traffic (Actual vs Fitted)')
plt.xlabel('Date')
plt.ylabel('Logged Traffic')
plt.legend()
plt.show()
No description has been provided for this image

Question: Do you think the model predicted well the web traffic performance of FourTex?


Model Diagnostics

Having estimated our marketing mix model, it is usually good practice to perform model diagnostic checks on the estimated residuals. In the marketing mix model above, we assume that residuals are uncorrelated (i.e. independent), have zero mean and constant variance. If the model passes these diagnostics, we conclude that the model is not misspecified and can be used to make statistical inferences and predictions.

The model output above provides two diagnostic tests: the Durbin-Watson (DW) statistic and the Jarque-Bera test. The Durbin-Watson (DW) statistic tests for autocorrelation in the residuals of a regression model, with values ranging from 0 to 4. A value of 2.0 suggests no autocorrelation. Values below 2 indicate positive autocorrelation, while values above 2 indicate negative autocorrelation. In our model, the DW statistic is found to be 1.38, indicating slight positive autocorrelation. In such cases, one may consider adding further lags to the lagged dependent variable and retest the autocorrelation in the residuals. The Jarque-Bera test is a statistical test that checks for the normality of the distribution of residuals in a dataset. Specifically, it tests the null hypothesis that the data is normally distributed, examining both the skewness (asymmetry) and kurtosis (tailedness) against what would be expected from a normal distribution. A lower value, such as .858, typically suggests that the deviation from normality is small. For a detailed review on how to conduct such diagnostic tests, the reader is referred to the chapter on demand forecasting.

Next, we turn our attention to the following questions that are central to the case study:

  • What drives the web traffic performance?

  • What is the traffic return on marketing investment?

  • What is the optimal budget allocation?

What drives web traffic performance?¶

What is the marketing’s contribution to web traffic performance? How much traffic was generated thanks to Google AdWords, Facebook, TV, and radio? To see this, first we need to convert the elasticities to unit effects using the following formula:

$$ \theta_i= \beta_i \frac{\bar{y}}{\bar{x_i}}, \hspace{2cm} \text{\small{i=\{AdWords, Facebook, TV, Radio\}}} $$

where ( \theta_i ) denotes the unit effect for advertising media ( i ), ( \beta_i ) is the estimated elasticity for media ( i ), ( \bar{y} ) is the baseline (average) traffic, ( \bar{x_i} ) is baseline advertising for media ( i ). Readers who are interested in learning how to derive this formula are referred to the Appendix.

For example, for Google AdWords, the formula above can be expressed as follows:

$$ \theta_{\text{AdWords}}= \beta_{\text{AdWords}}\ \frac{\text{Baseline Traffic}}{\text{Baseline AdWords}} $$

The following code chunk retrieves the coefficients (elasticities) from our log-log model output and then computes the unit effects.

In [42]:
# Retrieve each model coefficient
coefficients = regression_model.coef_
beta_adwords = coefficients[1]  # corresponding to ln_google_adwords
beta_facebook = coefficients[2]  # corresponding to ln_facebook
beta_tv = coefficients[3]  # corresponding to ln_tv
beta_radio = coefficients[4]  # corresponding to ln_radio

# Calculate the baseline (average) traffic
average_traffic = data['traffic'].mean()

# Calculate the baseline (average) advertising spending for each media
average_adwords = data['Google_Adwords'].mean()
average_facebook = data['Facebook'].mean()
average_tv = data['TV'].mean()
average_radio = data['Radio'].mean()

# Calculate the unit effects
theta_adwords = beta_adwords * (average_traffic / average_adwords)
theta_facebook = beta_facebook * (average_traffic / average_facebook)
theta_tv = beta_tv * (average_traffic / average_tv)
theta_radio = beta_radio * (average_traffic / average_radio)

Next, we compute the contribution of each advertising media to the overall traffic performance using the following formula:

$$ Contribution_i= \theta_i \sum_{t=1}^{T} x_t^{i}, \hspace{1cm} \forall i \in I $$

where ( \theta_i ) indicates the estimated unit effect for advertising media ( i ). ( x_t^{i} ) is the spending of advertising media ( i ) at time ( t ).

For example, for Google AdWords, we multiply the estimated unit effect (( \theta )) of Google AdWords (6.44) by the sum of Google AdWords spending. We compute the contribution of the other advertising media in the same way. The code following code chunk performs this task.

In [43]:
# Calculate total spending for each media channel
sum_adwords = data['Google_Adwords'].sum()
sum_facebook = data['Facebook'].sum()
sum_tv = data['TV'].sum()
sum_radio = data['Radio'].sum()

# Calculate each media's contribution to traffic
adwords_contribution = theta_adwords * sum_adwords
facebook_contribution = theta_facebook * sum_facebook
tv_contribution = theta_tv * sum_tv
radio_contribution = theta_radio * sum_radio

# Print the contribution of Adwords to traffic
print("Adwords Contribution to Traffic:", adwords_contribution)
print("Facebook Contribution to Traffic:", facebook_contribution)
print("TV Contribution to Traffic:", tv_contribution)
print("Radio Contribution to Traffic:", radio_contribution)
Adwords Contribution to Traffic: 57994698.52002453
Facebook Contribution to Traffic: 72261041.70891629
TV Contribution to Traffic: 1704751.0307449244
Radio Contribution to Traffic: 2597922.3139830157

We can show these media contributions graphically as well. We will use bar plots for this. To obtain the bar plots, we need to import two libraries called matplotlib and seaborn. If you have not installed them already in your computer you can just use:

! pip install 'library name'

ex: ! pip install seaborn

Just create a new chunk and run the updated version of this installation snippet.

We need to plug in the necessary data for the bar plot. The code chunk below generates the input for the bar plot.

In [44]:
import matplotlib.pyplot as plt
import seaborn as sns

# Bar plot information
media_contribution = [adwords_contribution, facebook_contribution, tv_contribution, radio_contribution]
media_contribution = np.round(media_contribution, decimals=0)
media_names = ["AdWords", "Facebook", "TV", "Radio"]
df = pd.DataFrame({'media_names': media_names, 'media_contribution': media_contribution})
df.head()
Out[44]:
media_names media_contribution
0 AdWords 57994699.0
1 Facebook 72261042.0
2 TV 1704751.0
3 Radio 2597922.0

The first and second lines of the code produce the media contribution column using the calculated contribution of each media. The third line of the code provides the media names together, and the fourth line of the code creates a data frame called df. Finally, the last line df.head() shows the data in jupyter. We will use the output of the above code to create a bar plot of the web traffic contribution of each media:

In [45]:
# Bar plot for media contribution to traffic
plt.figure(figsize=(8, 6))
sns.barplot(x='media_names', y='media_contribution', data=df, palette=['Red', 'Orange', 'Blue', 'Green'])
plt.title("Contribution to Traffic")
plt.xlabel("Media")
plt.ylabel("Contribution")
plt.show()
No description has been provided for this image

Let's delve into the Python code responsible for generating this bar chart. The command plt.figure(figsize=(8, 6)) initiates the creation of the plot with a specified width and height. The sns.barplot function is then called upon, with parameters that map media_names to the x-axis and media_contribution to the y-axis, sourced from the df dataframe. The color scheme for the bars is designated by the palette parameter. The plt.title, plt.xlabel, and plt.ylabel functions label the plot with a title and names for both axes. Lastly, plt.show() renders the plot visible.

This visualization reveals that a significant portion of web traffic is attributable to Facebook and Google AdWords initiatives, with Facebook being the chief driver. In contrast, Radio and TV show a minimal impact.

To convey the contributions more effectively, especially when the figures are substantial, we can calculate and present the contributions of each driver as a percentage. This method enables a clearer and more concise display of information, enhancing comprehensibility for the audience.

Next, we compute the media contributions in %.

In [46]:
# Calculate each media's contribution as a percentage
allmedia_contribution = sum(media_contribution)
pct_contribution = [x / allmedia_contribution for x in media_contribution]
pct_contribution = pd.Series(pct_contribution).apply(lambda x: f"{x:.2%}")

df2 = pd.DataFrame({'media_names': media_names, 'pct_contribution': pct_contribution})
df2
Out[46]:
media_names pct_contribution
0 AdWords 43.10%
1 Facebook 53.70%
2 TV 1.27%
3 Radio 1.93%

Finally, we obtain the bar plot that shows media contribution in percentage terms:

In [47]:
# Bar plot for media contribution to traffic in percentage
plt.figure(figsize=(8, 6))

# Use the numeric values for y-axis
df2['pct_contribution'] =  [float(i[:-1]) for i in (df2['pct_contribution'].tolist())]
sns.barplot(x='media_names', y=df2['pct_contribution'].astype(float), data=df2, palette=['Orange', 'Red', 'Blue', 'Green'])

# Adding labels manually
for index, value in enumerate(df2['pct_contribution']):
    plt.text(index, value, f"{value/100:.2%}", color='black', ha="center")

plt.title("Contribution to Traffic in %")
plt.xlabel("Media")
plt.ylabel("Contribution (%)")
plt.show()
No description has been provided for this image

The bar plot above suggests that 53.7% of the web traffic is driven by Facebook, 43.1% is driven by Google AdWords, 1.9% by radio, and 1.3% by TV.

Return on Marketing Investment¶

Does performance increase most with a $£1$ reduction in TV ads,or by increasing social media ads by $£1$ ? Financially oriented marketing executives are very often concerned about the return on marketing investment (ROMI). That is, they would like to know how much they earn with respect to how much they spend. Usually, the return metric is sales, revenues, or profits. However, it can also be something non-financial, e.g. customer engagement, web traffic and store traffic.

For FourTex, we focus on the traffic return on marketing investment (TROMI). The first input we need is the cost data. We obtain it using the following code chunk.

In [48]:
# Calculate the cost of each media
cost_adwords = sum_adwords
cost_facebook = sum_facebook
cost_tv = sum_tv
cost_radio = sum_radio
cost_total = cost_adwords + cost_facebook + cost_tv + cost_radio

cost = [cost_adwords, cost_facebook, cost_tv, cost_radio]
cost = np.round(cost, decimals=0)

We run the following code to get the input for the barplot of traffic contribution and cost incurred.

In [49]:
# Convert media_contribution and cost to lists if they aren't already
media_contribution_list = list(media_contribution)
cost_list = list(cost)

# Now, create the DataFrame
df3 = pd.DataFrame({
    'traf_cost': ['Traffic']*4 + ['Cost']*4,
    'media_names': media_names * 2,
    'values': media_contribution_list + cost_list
})

# Let's check the dataframe
df3.head(n=10)
Out[49]:
traf_cost media_names values
0 Traffic AdWords 57994699.0
1 Traffic Facebook 72261042.0
2 Traffic TV 1704751.0
3 Traffic Radio 2597922.0
4 Cost AdWords 8999557.0
5 Cost Facebook 6437100.0
6 Cost TV 41593933.0
7 Cost Radio 10939111.0

Next, we run the following code chunk to see the traffic return and cost data together in a barplot:

In [ ]:
# Bar plot for Traffic vs. Media Cost
# Below there is an easier version this is just for being fancy
# Bar plot for Traffic vs. Media Cost
plt.figure(figsize=(10, 6))
sns.barplot(
    x='media_names',
    y='values',
    hue='traf_cost',
    data=df3,
    palette=['turquoise', 'salmon'],  # Turquoise and salmon colors
    edgecolor='black'  # Black outline for each bar
)
plt.title("Traffic vs. Media Cost")
plt.xlabel("Media")
plt.ylabel("Traffic and Cost")
plt.legend(title='traf_cost')
plt.show()

# Simpler version of the graph
# plt.figure(figsize=(10, 6))
# sns.barplot(x='media_names', y='values', hue='traf_cost', data=df3, palette=['#1f77b4', '#ff7f0e'])  # Adjusted colors to match the first plot
# plt.title("Traffic vs. Media Cost")
# plt.xlabel("Media")
# plt.ylabel("Traffic and Cost")
# plt.legend(title='traf_cost')
# plt.show()
No description has been provided for this image

Discussion: What is your conclusion with regard to traffic generated and cost incurred? Which media returns the most and least?


Instead of showing cost and return data together, we can just compute the TROMI, and show the results in percentages. To do so, we need to divide the traffic contribution of each media by the cost of each media:

In [50]:
# Calculate the traffic return for each media
roi_adwords = adwords_contribution / cost_adwords
roi_facebook = facebook_contribution / cost_facebook
roi_tv = tv_contribution / cost_tv
roi_radio = radio_contribution / cost_radio

Next, we get the input for the TROMI barplot via the following code chunk.

In [ ]:
# TROMI Plot input
roi = [roi_adwords, roi_facebook, roi_tv, roi_radio]
roi = np.round(roi, decimals=0)

df4 = pd.DataFrame({'media_names': media_names, 'roi': roi})
df4.head()
Out[ ]:
media_names roi
0 AdWords 6.0
1 Facebook 11.0
2 TV 0.0
3 Radio 0.0

Finally, we obtain the bar plot for the TROMI analysis:

In [ ]:
# TROMI bar plot
plt.figure(figsize=(10, 6))
sns.barplot(x='media_names', y='roi', data=df4, palette=['Red', 'Orange', 'Blue', 'Green'])
plt.title("Traffic Return on Marketing Investment")
plt.xlabel("Media")
plt.ylabel("TROMI")
plt.show()
No description has been provided for this image

What does this figure tell us? In a nutshell, it suggests that for every $£1$ spent on Facebook, the expected number of web visits is $11$. Similarly, for every $£1$ spent on Google AdWords campaigns, we expect $6$ visits to occur.


Question: Are you surprised that the traffic return on investment for TV and radio is zero? Why do you think that the company invested so much in TV and radio channels although their traffic return is absent?


Marketing budget allocation¶

Marketing analysts follow to main approaches to guide their resource allocation strategies. One of them is normative decision making based on constrained optimization models (e.g. profit maximization subject to budget constraints).Another method is elasticity-based allocation. In this R application, we will allocate the marketing budget of FourTex by making use of the elasticities obtained from the log-log regression model.

Before diving into the optimal resource allocation, let's see what the current budget allocation looks like:

In [51]:
# Actual Budget Spending
costshare_adwords = cost_adwords / cost_total
costshare_facebook = cost_facebook / cost_total
costshare_tv = cost_tv / cost_total
costshare_radio = cost_radio / cost_total
In [53]:
# Pie chart for Actual Ad Spending
slices_actual = [costshare_adwords, costshare_facebook, costshare_tv, costshare_radio]
labels_actual = [f"{name} {share:.0%}" for name, share in zip(media_names, slices_actual)]
colors = ['#fb0007', '#73ff08', '#21ffff', '#6b00ff']  # Adjusted color hex codes

plt.figure(figsize=(6, 6))
plt.pie(slices_actual, labels=labels_actual, colors=colors, autopct='%1.1f%%', startangle=140)
plt.title("Actual Ad Spending")
plt.show()
No description has been provided for this image

This pie-chart tells us that 61% of the marketing budget was used for TV ads while 16% of the budget went to radio campaigns, 13% for Google AdWords, and 9% for Facebook. Given our findings on the media elasticities, how would you spend the budget? Would you spend so much money on TV ads to boost web traffic?

For the optimal allocation, we will use the estimated coefficients (( \beta )'s) from the log-log regression model. Recall that those coefficients are elasticities.

We calculate the optimal allocation for each media using the following formula:

$$ \text{Optimal Allocation}_i= \frac{\beta_i}{\sum_{i=1}^{I}{\beta_i}}, \hspace{1.5cm} \text{\small{i=\{AdWords, Facebook, TV, Radio\}}} $$

As an example, for Google AdWords, we will do it as follows:

$$ \text{Optimal Allocation}_{\text{AdWords}}= \frac{\beta_{\text{AdWords}}}{\beta_{\text{AdWords}} + \beta_{\text{Facebook}} + \beta_{\text{TV}} + \beta_{\text{Radio}}} $$

Let's do this in Jupyter now:

In [54]:
# The sum of all elasticities
beta_allmedia = beta_adwords + beta_facebook + beta_tv + beta_radio

# Optimal resource allocation
optim_adwords = beta_adwords / beta_allmedia
optim_facebook = beta_facebook / beta_allmedia
optim_tv = beta_tv / beta_allmedia
optim_radio = beta_radio / beta_allmedia

You can see the computed allocation using the Variable Inspector tab. Now, we can get a pie-chart that shows the allocation visually with percentages.

In [56]:
# Pie chart for Optimal Budget Allocation
slices_optim = [optim_adwords, optim_facebook, optim_tv, optim_radio]
labels_optim = [f"{name} {share:.0%}" for name, share in zip(media_names, slices_optim)]
In [57]:
plt.figure(figsize=(6, 6))
colors = ['#fb0007', '#73ff08', '#21ffff', '#6b00ff']
plt.pie(slices_optim, labels=labels_optim, colors=colors, autopct='%1.1f%%', startangle=140)
plt.title("Optimal Budget Allocation")
plt.show()
No description has been provided for this image

Question: What is your conclusion? Is the optimal allocation different from the actual spending? What would you suggest FourTex in terms of how they should deploy their marketing resources to boost the web traffic performance? Would the optimal allocation be different for a different performance metric (e.g., sales)?


We reach the end of this R application. To summarise, through the case study on FourTex, we explored how to:

  • assess the contribution of different advertising channels to web traffic performance
  • perform return on marketing investment analysis
  • allocate marketing budget optimally based on estimated elasticities.

Appendix¶

Here we demonstrate how unit (marginal) effects and elasticities are interrelated. Suppose that we have the following log-log model:

$$ \ln(y) = \beta_0 + \beta_1 \ln(x_1) + \epsilon $$

Note that we omit the subscript ( t ) to keep the notation simple. Solving for ( y ), we get:

$$ y = e^{\beta_0 + \beta_1 \ln(x_1) + \epsilon} $$

Next, we differentiate ( y ) with respect to ( x_1 ):

$$ \frac{dy}{dx_{1}} = \frac{\beta_{1}}{x_1} e^{\beta_0 + \beta_1 \ln(x_1) + \epsilon} $$

Since ( y = e^{\beta_0 + \beta_1 \ln(x_1) + \epsilon} ), we can express the equation above as follows:

$$ \frac{dy}{dx_{1}} = \frac{\beta_{1}}{x_1} y $$

Re-arranging the terms, we get the unit (marginal) effects as follows:

$$ \frac{dy}{dx_{1}} = \beta_{1} \frac{y}{x_1} $$

where ( y ) and ( x ) can be replaced by average values over a range.

It is straightforward to see that ( \beta_{1} ) is elasticity:

$$ \beta_{1} = \frac{dy}{dx_{1}} \frac{x_1}{y} $$